/*
  Copyright (c) 2018-2019 Oliver Galvin <odg at riseup dot net>
  This file is part of TubeMan.
  A local YouTube subscription manager and video browser.

 TubeMan is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 TubeMan is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with TubeMan.  If not, see <http://www.gnu.org/licenses/>.
 */

#define _POSIX_C_SOURCE 200809L			/*strdup, strndup        */
#define _FILE_OFFSET_BITS 64			/*Large file support     */
#define _TIME_BITS 64				/*64 bit time            */
#define _(string) gettext(string)		/*gettext alias          */
#ifdef NDEBUG
# define _FORTIFY_SOURCE 2			/*buffer overflow checks */
#endif
#ifdef HAVE_FUNC_ATTRIBUTE_UNUSED
# define UNUSED(x) __attribute__((unused))x
#else
# define UNUSED(x) x
#endif

/*      ANSI C      */
#include <stdio.h>		/*files: fopen, fclose, xprintf          */
#include <stdarg.h>		/*variadic functions: va_x, vprintf      */
#include <stdlib.h>		/*malloc, realloc, free, getenv          */
#include <string.h>		/*strings: str{cpy,len,str,chr,tok}      */
#include <errno.h>		/*error codes                            */
#include <time.h>		/*time, gmtime, mktime, time_t           */
#include <assert.h>		/*assertions used when debugging         */

/*      Windows     */
#ifdef _WIN32
# define CONFIGVAR "APPDATA"
# define DATAVAR "APPDATA"
# define USERVAR "USERNAME"
# define chdir(x) _chdir(x)
# define strdup(x) _strdup(x)
#else
# define CONFIGVAR "XDG_CONFIG_HOME"
# define DATAVAR "XDG_DATA_HOME"
# define USERVAR "USER"
#endif
#ifdef HAVE_WINDOWS_H
# define WIN32_LEAN_AND_MEAN    /*don't include extra libs               */
# include <windows.h>		/*CreateProcess, CloseHandle, GetUserName*/
#endif
#ifdef HAVE_DIRECT_H
# include <direct.h>		/*directories: _mkdir, _chdir            */
#endif
#if HAVE_MKDIR && MKDIR_TAKES_ONE_ARG
# define mkdir(a, b) mkdir(a)	/* MinGW32                               */
#elif HAVE__MKDIR
# define mkdir(a, b) _mkdir(a)	/* plain Windows                         */
#endif

/*       POSIX      */
#ifdef HAVE_UNISTD_H
# include <unistd.h>		/*POSIX: getuid, fork, pipe, execv, chdir*/
#endif
#ifdef HAVE_SYS_STAT_H
# include <sys/stat.h>		/*directories: mkdir                     */
#endif
#ifdef HAVE_SYS_TYPES_H
# include <sys/types.h>		/*types: size_t, pid_t, uid_t, mode_t    */
#endif
#ifdef HAVE_SYS_WAIT_H
# include <sys/wait.h>		/*waitpid                                */
#endif
#ifdef HAVE_SYS_IOCTL_H
# include <sys/ioctl.h>		/*ioctl                                  */
#endif
#ifdef HAVE_PWD_H
# include <pwd.h>		/*passwd, getpwuid                       */
#endif

/*   Dependencies   */
#include <curl/curl.h>		/*downloading data from the web: curl_*  */
#include <sqlite3.h>		/*embedded SQL database: sqlite3_*       */

/*      Curses      */
#if defined(HAVE_LIBMENU) && !defined(NOTUI)
# include <menu.h>
#elif !defined(NOTUI)
# define NOTUI			/*build without curses interactive mode  */
#endif

/*     Bundled      */
#ifdef HAVE_GETOPT_H		/*command line option parsing            */
# include <getopt.h>
#else
# include "ya_getopt.h"
#endif
#include "tubeman.h"		/*exposed function/struct definitions    */
#include "gettext.h"		/*gettext internationalisation           */
#include "ini.h"		/*parsing .ini configuration file        */
#include "data.h"		/*static data used by tubeman            */
#include "jsmn.h"		/*parsing/tokenising JSON data           */
#define toklen(x) (size_t)(x->end - x->start)

/*Macros that set colours if colours are enabled in the config*/
#define RESET if(config.colour) {printf("\x1B[00m");}
#define BOLD  if(config.colour) {printf("\x1B[01m");}
#define RED   if(config.colour) {printf("\x1B[31m");}
#define GRN   if(config.colour) {printf("\x1B[32m");}
#define YEL   if(config.colour) {printf("\x1B[33m");}
#define BLU   if(config.colour) {printf("\x1B[34m");}
#define MAG   if(config.colour) {printf("\x1B[35m");}
#define CYN   if(config.colour) {printf("\x1B[36m");}
#define WHT   if(config.colour) {printf("\x1B[37m");}

typedef struct MemoryStruct {
	char *memory;
	size_t size;
} MemoryStruct;

static aconf config;
static sqlite3 *viddb, *subdb;

/*String function portability*/
#ifndef HAVE_STRCHR
# define strchr(a,b) index(a,b)
# define strrchr(a,b) rindex(a,b)
#endif
#ifndef HAVE_STRDUP
static char* strdup(const char *str)
{
	char *buf;
	if(!str) {
		return NULL;
	}
	buf = (char *)malloc(strlen(str) + 1);
	if(buf) {
		strcpy(buf, str);
	}
	return buf;
}
#endif
#ifndef HAVE_STRNDUP
static char *strndup(const char *str, size_t len)
{
	char *buf;
	buf = (char *)malloc(len + 1);
	if(buf) {
		strncpy(buf, str, len);
		buf[len] = '\0';
	}
	return buf;
}
#endif

/*Verbose output - printf only when verbose mode is on*/
static void verbose(const char *fmt, ...)
{
	if(config.verbose) {
		va_list args;
		va_start(args, fmt);
		vprintf(fmt, args);
		va_end(args);
	}
}

/*Error output - output a highlighted error message to stderr*/
static void emsg(const char *fmt, ...)
{
	va_list args;
	RED
	fprintf(stderr, _("Error: "));
	RESET
	va_start(args, fmt);
	vfprintf(stderr, _(fmt), args);
	va_end(args);
}

/*Write curl output into memory
  Based on code here: https://curl.haxx.se/libcurl/c/getinmemory.html*/
/* Returns the number of bytes processed*/
static size_t curlMemory(void *contents, size_t size,
			 size_t nmemb, void *userp)
{
	size_t realsize = size * nmemb;
	MemoryStruct *mem = (MemoryStruct *)userp;
	mem->memory = (char *)realloc(mem->memory, mem->size + realsize + 1);
	if(mem->memory == NULL) {
		emsg("not enough memory\n");
		return 0;
	}
	memcpy(&(mem->memory[mem->size]), contents, realsize);
	mem->size += realsize;
	mem->memory[mem->size] = 0;
	return realsize;
}

/*Write curl output into file*/
static size_t curlFile(void *ptr, size_t size, size_t nmemb, void *stream)
{
	return fwrite(ptr, size, nmemb, (FILE *)stream);
}

/*Set curl handler user agent string, and proxy if configured*/
static void curlSetup(CURL *curl)
{
	int n = rand() % MAXUANUM;
	if(config.proxy && config.ip && config.port) {
		verbose(_("Using a proxy at IP %s, port %li\n"),
			config.ip, config.port);
		curl_easy_setopt(curl, CURLOPT_PROXYTYPE, config.proxy);
		curl_easy_setopt(curl, CURLOPT_PROXY, config.ip);
		curl_easy_setopt(curl, CURLOPT_PROXYPORT, config.port);
	}
	verbose(_("User agent: %s\n"), useragents[n]);
	curl_easy_setopt(curl, CURLOPT_USERAGENT, useragents[n]);
}

/*Free a list of arguments*/
static void freeargs(const int argc, char *args[])
{
	int i;
	for(i=0; i<argc; i++) {
		if(args[i]) {free(args[i]);}
	}
}

/*Free anything in an avideo struct*/
static void freevideo(avideo video)
{
	if(video.vid) {free(video.vid);}
	if(video.title) {free(video.title);}
	if(video.desc) {free(video.desc);}
}

/*Free anything in an achannel struct*/
static void freechannel(achannel chan)
{
	if(chan.url) {free(chan.url);}
	if(chan.name) {free(chan.name);}
	if(chan.thumb) {free(chan.thumb);}
	if(chan.desc) {free(chan.desc);}
}

/*Execute given program with given argv, safely and portably*/
#if HAVE_UNISTD_H && HAVE_SYS_WAIT_H && HAVE_SYS_TYPES_H
static int execprog(const int UNUSED(argc), char *argv[], const char *pipestr)
{
	int fd[2];
	pid_t pid;
	assert(argv[argc] == NULL);
	if(pipestr) {
		if(pipe(fd) == -1) {
			emsg("pipe failed\n");
			return 1;
		}
	}
	pid = fork();
	if(pid < 0) {            /*Error*/
		emsg("cannot create new process\n");
		return 1;
	} else if(pid > 0) {     /*Parent process*/
		int status;
		pid_t ret;
		if(pipestr) {
			close(fd[0]);
			if(write(fd[1], pipestr, strlen(pipestr)) == -1) {
				emsg("could not write to pipe\n");
				return 1;
			}
			close(fd[1]);
		}
		while((ret = waitpid(pid, &status, 0)) == -1) {
			if(errno != EINTR) {
				emsg("waitpid returned -1\n");
				break;
			}
		}
		if ((ret != -1) &&
		    (!WIFEXITED(status) || !WEXITSTATUS(status)) ) {
			return 1;
		}
	} else {                 /*Child process*/
		if(pipestr) {
			close(fd[1]);
			dup2(fd[0], STDIN_FILENO);
			close(fd[0]);
		}
		if(execvp(argv[0], argv) == -1) {
			emsg("command not found - %s\n", argv[0]);
			return 127;
		}
	}
#elif HAVE_WINDOWS_H
static int execprog(const int argc, char *argv[], const char *pipestr)
{
	STARTUPINFO si = { 0 };
	PROCESS_INFORMATION pi;
	int i;
	char *temp, *command;
	char *program = (char *)malloc(strlen(argv[0]) + 5);
	assert(argv[argc] == NULL);
	sprintf(program, "%s.exe", argv[0]);
	command = strdup(program);
	for(i=1; i<argc; i++) {
		temp = (char *)realloc(command, strlen(argv[i]) + 2);
		if(!temp) {
			emsg("out of memory\n");
			free(program);
			free(command);
			return 1;
		} else {
			command = temp;
		}
		sprintf(command + strlen(command), " %s", argv[i]);
	}
	si.cb = sizeof(si);
	if (!CreateProcess(TEXT(program), command, \
	    NULL, NULL, FALSE, 0, 0, 0, &si, &pi)) {
		emsg("cannot create new process\n");
		free(program);
		free(command);
		return 1;
	}
	CloseHandle(pi.hThread);
	CloseHandle(pi.hProcess);
	free(program);
	free(command);
#else
static int execprog(const int UNUSED(argc),
		    char UNUSED(*argv[]),
		    const char UNUSED(*pipestr))
{
	return 1;
#endif
	return 0;
}

/*Returns the raw HTML page from a given URL*/
static char* gethtml(const char *url, struct curl_slist *headers)
{
	CURL *curl;
	CURLcode res;
	MemoryStruct page;
	char *html = NULL;
	if(!url) {
		curl_slist_free_all(headers);
		return NULL;
	}
	page.memory = malloc(1);
	page.size = 0;
	curl = curl_easy_init();
	curl_easy_setopt(curl, CURLOPT_URL, url);
	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlMemory);
	curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&page);
	curlSetup(curl);
	headers = curl_slist_append(headers, "DNT: 1"); /*Always Do Not Track*/
	curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
	res = curl_easy_perform(curl);
	if(res != CURLE_OK) {
		emsg("connection failed - %s\n", curl_easy_strerror(res));
		curl_slist_free_all(headers);
		return NULL;
	}
	if(page.memory) {
		html = curl_easy_unescape(curl, page.memory, 0, NULL);
		free(page.memory);
	}
	curl_slist_free_all(headers);
	curl_easy_cleanup(curl);
	return html;
}

/*Return JSON data from given Youtube URL*/
static char* getyoutubejson(const char *url)
{
	char *json = NULL;
	char *html = NULL;
	char *temp = NULL;
	struct curl_slist *list = NULL;
	list = curl_slist_append(list,
		"X-YouTube-Client-Name: "YTCLIENTNAME);
	list = curl_slist_append(list,
		"X-YouTube-Client-Version: "YTCLIENTVER);
	if(!(html = gethtml(url, list))) {
		return NULL;
	}
	if((temp = strstr(html, "\"ytInitialData\""))) {
		if((temp = strtok(temp + 19, ";"))) {
			json = strdup(temp);
		}
	}
	curl_free(html);
	return json;
}

/*Get the VID/CID from a URL*/
static char* geturlid(const char *url)
{
	char *id = NULL, *tidy = NULL;
	if(!url) {return NULL;}
	id = strchr(url, '=');
	if(id) {
		id++;
	} else {
		id = strstr(url, ".be/");
		if(id) {
			id += 4;
		} else {
			id = strstr(url, "/channel/");
			if(id) {
				id += 9;
			} else {
				id = strstr(url, "/embed/");
				if(id) {
					id += 7;
				}
			}
		}
	}
	if(id) {
		tidy = strtok(id, "?/");
		if(tidy) {return tidy;}
	}
	return id;
}

/*Check if the given key is where the given jsmn token points to in js*/
/*  Returns 1 when found, 0 otherwise*/
static int findkey(const char *js, const jsmntok_t *t, const char *key)
{
	if(key) {
		if(strlen(key) == toklen(t) &&
		   !strncmp(js + t->start, key, toklen(t))) {
			return 1;
		}
	}
	return 0;
}

static size_t searchjson(const char *js, const jsmntok_t *t, const char *key,
		      const int index, const size_t count, const int indent);

/*Find the 'index'th object in a JSON array*/
/*  Returns the number of jsmn tokens offset from t that points to the result*/
static size_t searchjsonarray(const char *js, const jsmntok_t *t,
			      const int index, const size_t c,
			      const int indent)
{
	int i;
	size_t j = 0;
	if(!t || t->type != JSMN_ARRAY) {
		return 0;
	}
	for (i = 0; i < t->size; i++) {
		if(i == index) {
			break;
		}
		j += searchjson(js, t+1+j, NULL, -1, c-j, indent+1);
	}
	return (size_t)j+1;
}

/*Search the given jsmn token for the attribute 'key'*/
/*  Returns the number of tokens to skip to get to given key*/
static size_t searchjson(const char *js, const jsmntok_t *t, const char *key,
		         const int index, const size_t c, const int indent)
{
	if (!c) {
		return 0;
	}
	if (t->type == JSMN_PRIMITIVE ||
	    t->type == JSMN_STRING) {
		return 1;
	} else if (t->type == JSMN_OBJECT) {
		int i;
		size_t j = 0;
		for (i = 0; i < t->size; i++) {
			j += searchjson(js, t+1+j, NULL, -1, c-j, indent+1);
			if(!indent && findkey(js, t+j, key)) {
				break;
			}
			j += searchjson(js, t+1+j, key, index, c-j, indent+1);
		}
		return j+1;
	} else if (t->type == JSMN_ARRAY) {
		return searchjsonarray(js, t, -1, c, indent+1);
	}
	return 0;
}

/*Find the jsmn token for the JSON object of the key, at the given index*/
/*  Returns pointer to jsmn token*/
static jsmntok_t* findtok(const char *js, jsmntok_t *t, const char *key,
			  const size_t c, const int index)
{
	jsmntok_t *ret = t;
	ret += searchjson(js, t, key, index, c, 0);
	ret += searchjsonarray(js, ret, index, c, 0);
	return ret;
}

/*Get video description, timestamp, duration, view count, related vids
static char* updvidinfo(char *data)
{
	related vids: '.contents.twoColumnWatchNextResults.secondaryResults.
	secondaryResults.results[].compactVideoRenderer.title.simpleText'
	return NULL;
}*/

/*Get latest video listing for a given channel, update database if necessary*/
/* Returns number of new videos*/
static int updvidlist(const char *cid)
{
	char *url, *cmd, *json, *cont, latest[16], *errmsg;
	avideo newvid = {0};
	int len, err, i;
	size_t cidlen = strlen(cid);
	size_t jsonlen, n;
	jsmn_parser p;
	jsmntok_t *top, *t;

	strcpy(latest, ""); /*temp, get the latest vid*/
	/*potentially parse rss feeds here*/
	/*old code used by expat - NOTE: delete
	const time_t stamp = 0;
	struct tm *t;
	char *val = (char *)data;
	if(!strcmp(tag, "entry")) {
		initvideo(curvid
	} else if(!strcmp(tag, "yt:videoId")) {
		curvid.vid = strdup(data);
	} else if(!strcmp(tag, "media:title")) {
		curvid.title = strdup(data);
	} else if(!strcmp(tag, "published")) {
		t = gmtime(&stamp);
		strptime(val, "%z", t);
		curvid.timestamp = mktime(t);
	} else if(!strcmp(tag, "media:description")) {
		curvid.desc = strdup(data);
	}*/

	/*Now try the playlist(s)*/
	url = (char *)malloc(39 + cidlen);
	cid += 2;
	sprintf(url, "https://www.youtube.com/playlist?list=UU%s", cid);
	if(!(json = getyoutubejson(url))) {
		emsg("no JSON data found\n");
		return -1;
	}
	cid -= 2;
	jsonlen = strlen(json);
	jsmn_init(&p);
	n = (size_t)jsmn_parse(&p, json, jsonlen, NULL, 0);
	if(n < 1) {
		emsg("could not parse JSON\n");
		return -1;
	}
	top = (jsmntok_t *)malloc(sizeof(jsmntok_t)*(size_t)n);
	jsmn_init(&p);
	n = (size_t)jsmn_parse(&p, json, jsonlen, top, (size_t)n);
	t = findtok(json, top, "contents", n, 0);
	t = findtok(json, t, "twoColumnBrowseResultsRenderer", n, 0);
	t = findtok(json, t, "tabs", n, 0);
	t = findtok(json, t, "tabRenderer", n, 0);
	t = findtok(json, t, "content", n, 0);
	t = findtok(json, t, "sectionListRenderer", n, 0);
	t = findtok(json, t, "contents", n, 0);
	t = findtok(json, t, "itemSectionRenderer", n, 0);
	t = findtok(json, t, "contents", n, 0);
	t = findtok(json, t, "playlistVideoListRenderer", n, 0);
	sqlite3_exec(viddb, "BEGIN TRANSACTION", NULL, NULL, &errmsg);
	cmd = (char *)malloc(170 + cidlen);
	len = sprintf(cmd, "CREATE TABLE IF NOT EXISTS %s (", cid);
	len = sprintf(cmd + len, "id INTEGER PRIMARY KEY AUTOINCREMENT, ");
	len = sprintf(cmd + len, "vid TEXT, ");
	len = sprintf(cmd + len, "title TEXT, ");
	len = sprintf(cmd + len, "duration INTEGER, ");
	len = sprintf(cmd + len, "timestamp INTEGER, ");
	len = sprintf(cmd + len, "desc TEXT, ");
	len = sprintf(cmd + len, "viewed INTEGER, ");
	len = sprintf(cmd + len, "licence text);");
	err = sqlite3_exec(viddb, cmd, NULL, NULL, &errmsg);
	free(cmd);
	if(err) {
		emsg("SQL Error: %s\n", errmsg);
		sqlite3_free(errmsg);
		return err;
	}
	i = 0;
	while(strcmp(latest, newvid.vid) && i < 100) {
		jsmntok_t *u, *v;
		u = findtok(json, t, "contents", n, i);
		u = findtok(json, u, "playlistVideoRenderer", n, 0);
		v = findtok(json, u, "videoId", n, 0);
		newvid.vid = strndup(json + v->start, toklen(v));
		v = findtok(json, u, "title", n, 0);
		v = findtok(json, v, "SimpleText", n, 0);
		newvid.title = strndup(json + v->start, toklen(v));
		v = findtok(json, u, "lengthSeconds", n, 0);
		newvid.duration = strtol(json + v->start, NULL, 10);
		/*can't get timestamp, duration or licence yet*/
		newvid.desc = strdup("temp");
		newvid.timestamp = 0;
		newvid.viewed = 0;
		newvid.licence = strdup("Standard Youtube");

		len = sprintf(cmd, "INSERT INTO %s VALUES(",
			cid);
		len = sprintf(cmd + len, "%i, '%s', ",
			i, newvid.vid);
		len = sprintf(cmd + len, "'%s', %li, ",
			newvid.title, newvid.duration);
		len = sprintf(cmd + len, "%li, '%s', ",
			newvid.timestamp, newvid.desc);
		len = sprintf(cmd + len, "%i, '%s');",
			newvid.viewed, newvid.licence);
		sqlite3_exec(viddb, cmd, NULL, NULL, &errmsg);
		i++;
	}
	if(!strcmp(latest, newvid.vid)) {
		sqlite3_exec(viddb, "END TRANSACTION", NULL, NULL, &errmsg);
		sqlite3_free(errmsg);
		freevideo(newvid);
		free(url);
		return i;
	}
	t = findtok(json, t, "continuations", n, 0);
	t = findtok(json, t, "nextContinuationData", n, 0);
	t = findtok(json, t, "continuation", n, 0);
	cont = strndup(json + t->start, toklen(t));
	/*download: https://www.youtube.com/browse_ajax?ctoken=$cont
	  request header:	"X-YouTube-Client-Version: 2.20180117"
	  			"X-YouTube-Client-Name: 1"*/

	sqlite3_exec(viddb, "END TRANSACTION", NULL, NULL, &errmsg);
	sqlite3_free(errmsg);
	freevideo(newvid);
	free(top);
	free(cont);
	free(url);
	return i;
}

/*Subscribe to a channel, getting necessary info, and create video table*/
int addchannel(const char *cid, const char *name)
{
	achannel newchan = {0};
	int err = 0;
	size_t jsonlen, n;
	char *json, *abouturl, *cmd, *errmsg, *temp, *subcount;
	jsmn_parser p;
	jsmntok_t *top, *t, *u;

	abouturl = (char *)malloc(40 + strlen(cid));
	sprintf(abouturl, "https://www.youtube.com/channel/%s", cid);
	newchan.url = strdup(abouturl);
	strcat(abouturl, "/about");
	if(!(json = getyoutubejson(abouturl))) {
		emsg("no JSON data found\n");
		return 1;
	}
	free(abouturl);

	jsonlen = strlen(json);
	jsmn_init(&p);
	n = (size_t)jsmn_parse(&p, json, jsonlen, NULL, 0);
	if(n < 1) {
		emsg("could not parse JSON\n");
		return 1;
	}
	top = (jsmntok_t *)malloc(sizeof(jsmntok_t)*n);
	jsmn_init(&p);
	n = (size_t)jsmn_parse(&p, json, jsonlen, top, n);

	t = findtok(json, top, "contents", n, 0);
	t = findtok(json, t, "twoColumnBrowseResultsRenderer", n, 0);
	t = findtok(json, t, "tabs", n, 5);
	t = findtok(json, t, "tabRenderer", n, 0);
	t = findtok(json, t, "content", n, 0);
	t = findtok(json, t, "sectionListRenderer", n, 0);
	t = findtok(json, t, "contents", n, 0);
	t = findtok(json, t, "itemSectionRenderer", n, 0);
	t = findtok(json, t, "contents", n, 0);
	t = findtok(json, t, "channelAboutFullMetadataRenderer", n, 0);

	if(!name) {
		u = findtok(json, t, "title", n, 0);
		u = findtok(json, u, "simpleText", n, 0);
		newchan.name = strndup(json + u->start, toklen(u));
	} else {
		newchan.name = strdup(name);
	}
	u = findtok(json, t, "description", n, 0);
	u = findtok(json, u, "simpleText", n, 0);
	newchan.desc = strndup(json + u->start, toklen(u));
	u = findtok(json, t, "avatar", n, 0);
	u = findtok(json, u, "thumbnails", n, 0);
	u = findtok(json, u, "url", n, 0);
	newchan.thumb = strndup(json + u->start, toklen(u));
	u = findtok(json, t, "subscriberCountText", n, 0);
	u = findtok(json, u, "runs", n, 0);
	u = findtok(json, u, "text", n, 0);
	subcount = strndup(json + u->start, toklen(u));
	/*Remove comma(s) first*/
	temp = subcount;
	while((temp = strchr(temp, ','))) {
		memmove(temp, temp+1, strlen(temp+1)+1);
	}
	newchan.subs = strtol(subcount, NULL, 10);
	free(subcount);
	free(top);
	free(json);
	cmd = (char *)malloc(64 + strlen(newchan.url) + strlen(newchan.name) +
			     strlen(newchan.thumb) + strlen(newchan.desc));
	sprintf(cmd,
	    "INSERT INTO subscriptions VALUES(%li,0,'%s','%s','%s',%li,'%s')",
	    newchan.id, newchan.url, newchan.name,
	    newchan.thumb, newchan.subs, newchan.desc);
	/*err = sqlite3_exec(subdb, cmd, NULL, NULL, &errmsg);*/
	printf("%s\n", cmd);
	free(cmd);
	/*if(err) {
		emsg("SQL Error: %s\n", errmsg);
		sqlite3_free(errmsg);
		return err;
	}
	sqlite3_free(errmsg);*/
	freechannel(newchan);
	/*err = updvidlist(cid);*/
	return err;
}

/*Run for each record in an SQL query, reads record into an achannel struct*/
static int readchannel(void *data, int argc, char **argv, char **ColName)
{
	int i;
	char **err = 0;
	achannel *found = (achannel *)data;
	/*Loop through each field in the given record*/
	for(i=0; i<argc; i++) {
		if(!strcmp(ColName[i], "service_id")) {
			found->x = 0;
		} else if(!strcmp(ColName[i], "uid")) {
			found->id = strtol(argv[i], err, 10);
		} else if(!strcmp(ColName[i], "url")) {
			found->url = strdup(argv[i]);
		} else if(!strcmp(ColName[i], "name")) {
			found->name = strdup(argv[i]);
		} else if(!strcmp(ColName[i], "avatar_url")) {
			found->thumb = strdup(argv[i]);
		} else if(!strcmp(ColName[i], "subscriber_count")) {
			found->subs = strtol(argv[i], err, 10);
		} else if(!strcmp(ColName[i], "description")) {
			found->desc = strdup(argv[i]);
		}
	}
	return 0;
}

/*Remove channel from subscription database, and associated video list*/
int delchannel(const char *term)
{
	int err = 0;
	char *cmd, *cid, *errmsg = NULL;
	achannel chan = {0};
	
	/*Get CID of the channel referred to*/
	printf(_("Finding the channel...\n"));
	cmd = (char *)malloc(64 + 2*strlen(term));
	sprintf(cmd, "SELECT url, name FROM subscriptions \
		WHERE uid='%s' OR url='%%%s';", term, term);
	err = sqlite3_exec(subdb, cmd, readchannel, &chan, &errmsg);
	free(cmd);
	if(err) {
		emsg("SQL Error: %s\n", errmsg);
	}
	if(!chan.url) {
		emsg("channel not found!\n");
		return 1;
	}
	cid = geturlid(chan.url);

	/*Delete channel from subscriptions*/
	printf(_("Deleting the subscription to '%s'...\n"), chan.name);
	cmd = (char *)malloc(55 + strlen(term) + strlen(cid));
	sprintf(cmd, "DELETE FROM subscriptions \
		WHERE uid='%s' OR url='%%%s';", term, cid);
	err = sqlite3_exec(subdb, cmd, NULL, NULL, &errmsg);
	free(cmd);
	if(err) {
		emsg("SQL Error: %s\n", errmsg);
	}

	/*Delete relevant video table from video database*/
	printf(_("Deleting the channel's video list...\n"));
	cmd = (char *)malloc(13 + strlen(cid));
	sprintf(cmd, "DROP TABLE %s;", cid);
	err = sqlite3_exec(viddb, cmd, NULL, NULL, &errmsg);
	free(cmd);
	if(err) {
		emsg("SQL Error: %s\n", errmsg);
	}

	printf(_("Done.\n"));
	freechannel(chan);
	sqlite3_free(errmsg);
	return err;
}

/*Get current number of subscriptions*/
static int getsubnum(void)
{
	int err = 0;
	int count = 0;
	const char *cmd = "SELECT COUNT(*) FROM subscriptions";
	sqlite3_stmt *stmt;
	err = sqlite3_prepare_v2(subdb, cmd,
				 (int)strlen(cmd) + 1, &stmt, NULL);
	if(err) {
		emsg("could not compile statement\n");
		return -1;
	}
	if(sqlite3_step(stmt) != SQLITE_ROW) {
		emsg("could not read any data\n");
		return -1;
	}
	count = sqlite3_column_int(stmt, 0);
	sqlite3_finalize(stmt);
	return count;
}

/*Search subscription table, returning array of channels*/
int searchchannels(const char *term, achannel **output)
{
	int err, i, j, cols, rows;
	achannel *chan = NULL;
	sqlite3_stmt *stmt;
	char *cmd;
	const char *field;
	if(!term) {
		cmd = strdup("SELECT * FROM subscriptions;");
	} else {
		cmd = (char *)malloc(100 + strlen(term) * 2);
		strcpy(cmd, "SELECT * FROM subscriptions ");
		sprintf(cmd + strlen(cmd), "WHERE name LIKE '%%%s%%' \
			OR description LIKE '%%%s%%';", term, term);
	}
	err = sqlite3_prepare_v2(subdb, cmd,
				 (int)strlen(cmd) + 1, &stmt, NULL);
	free(cmd);
	if(err) {
		emsg("could not compile statement\n");
		return -1;
	}
	cols = sqlite3_column_count(stmt);
	rows = getsubnum();
	chan = (achannel *)malloc(sizeof(achannel) * (size_t)(rows + 1));
	i = 0;
	while(sqlite3_step(stmt) == SQLITE_ROW) {
		/*Initialise first*/
		chan[i].x = 0;
		chan[i].id = 0;
		chan[i].url = NULL;
		chan[i].name = NULL;
		chan[i].thumb = NULL;
		chan[i].subs = 0;
		chan[i].desc = NULL;
		for(j=0; j<cols; j++) {
			field = sqlite3_column_name(stmt, j);
			if(!strcmp(field, "service_id")) {
				chan[i].x = 0;
			} else if(!strcmp(field, "uid")) {
				chan[i].id = sqlite3_column_int(stmt, j);
			} else if(!strcmp(field, "url")) {
				chan[i].url = \
				strdup((const char*)sqlite3_column_text(stmt, j));
			} else if(!strcmp(field, "name")) {
				chan[i].name = \
				strdup((const char*)sqlite3_column_text(stmt, j));
			} else if(!strcmp(field, "avatar_url")) {
				chan[i].thumb = \
				strdup((const char*)sqlite3_column_text(stmt, j));
			} else if(!strcmp(field, "subscriber_count")) {
				chan[i].subs = \
				sqlite3_column_int(stmt, j);
			} else if(!strcmp(field, "description")) {
				chan[i].desc = \
				strdup((const char*)sqlite3_column_text(stmt, j));
			}
		}
		i++;
	}
	/*Last record is all NULL*/
	chan[i].x = 0;
	chan[i].id = 0;
	chan[i].url = NULL;
	chan[i].name = NULL;
	chan[i].thumb = NULL;
	chan[i].subs = 0;
	chan[i].desc = NULL;
	sqlite3_finalize(stmt);
	*output = chan;
	return rows;
}

/*Search local subscriptions and show results*/
static int showsearchresults(const char *term)
{
	achannel *chan = NULL;
	int i = 0;
	int rows = searchchannels(term, &chan);
	if(rows == -1) {
		return 1;
	}
	while(chan[i].name) {
		MAG
		BOLD
		printf("%li - ", chan[i].id);
		RESET
		BOLD
		printf("%s, ", chan[i].name);
		GRN
		printf(_("%li subscribers\n"), chan[i].subs);
		RESET
		printf("\t%s\n", chan[i].desc);
		freechannel(chan[i]);
		i++;
	}
	free(chan);
	if(i == 0) {
		emsg("no results found!\n");
		return 1;
	}
	return 0;
}

/*Show detailed information about a channel, if it is subscribed to*/
static int querychannel(const char *id)
{
	int err;
	long int uid;
	achannel chan = {0};
	char *cmd, *badchars = NULL, *errmsg = NULL;
	uid = strtol(id, &badchars, 10);
	if(*badchars != '\0') {
		emsg("invalid ID\n");
		return 1;
	}
	cmd = (char *)malloc(45 + strlen(id));
	sprintf(cmd, "SELECT * FROM subscriptions WHERE uid = %li;", uid);
	err = sqlite3_exec(subdb, cmd, readchannel, &chan, &errmsg);
	free(cmd);
	if(err) {
		emsg("SQL Error: %s\n", errmsg);
		sqlite3_free(errmsg);
		return err;
	}
	sqlite3_free(errmsg);
	if(!chan.url) {
		emsg("channel not found!\n");
		return 1;
	}
	BOLD
	printf("Name\t\t: ");
	RESET
	printf("%s\n", chan.name);
	BOLD
	printf("Subscribers\t: ");
	RESET
	printf("%li\n", chan.subs);
	BOLD
	printf("ID\t\t: ");
	RESET
	printf("%li\n", chan.id);
	BOLD
	printf("URL\t\t: ");
	RESET
	printf("%s\n", chan.url);
	BOLD
	printf("Avatar URL\t: ");
	RESET
	printf("%s\n", chan.thumb);
	BOLD
	printf("Description\t: ");
	RESET
	printf("%s\n", chan.desc);
	freechannel(chan);
	return 0;
}

/*Parse OPML file to merge Youtube susbscriptions into database*/
static int importopml(FILE *db)
{
	char *attr, *name = NULL, *cid, *data, *chunk, *temp;
	int MAX, SIZE;
	int err = 0;
	MAX = 1000;
	SIZE = MAX;
	chunk = (char *)malloc((size_t)MAX);
	if(!fgets(chunk, MAX, db)) {
		emsg("could not read any data from the file\n");
		free(chunk);
		return 1;
	}
	data = strdup(chunk);
	while(fgets(chunk, MAX, db)) {
		SIZE += MAX;
		temp = (char *)realloc(data, (size_t)SIZE);
		if(!temp) {
			emsg("out of memory\n");
			free(data);
			free(chunk);
			return 1;
		} else {
			data = temp;
		}
		strcat(data, chunk);
	}
	attr = strtok(data, " =");
	while((attr = strtok(NULL, " ="))) {
		if(!strcmp(attr, "title")) {
			name = strtok(NULL, "\"");
		} else if(!strcmp(attr, "xmlUrl")) {
			cid = strtok(NULL, "=");
			/*cppcheck-suppress redundantAssignment*/
			cid = strtok(NULL, "\"");
			err = addchannel(cid, name);
		}
	}
	free(data);
	free(chunk);
	return err;
}

/*Merge given SQLite/OPML database into current subscription database*/
int importdb(const char *filename)
{
	char line[7], *tok;
	int err = 0;
	FILE *file = fopen(filename, "r");
	if(file == NULL) {
		emsg("failed to open file.\n");
		return 1;
	}
	if(!fgets(line, 7, file)) {
		emsg("could not read any data from the file\n");
		fclose(file);
		return 1;
	}
	tok = strtok(line, " ");
	if(!strcmp(tok, "<opml")) {
		err = importopml(file);
	} else if(!strcmp(tok, "SQLite")) {
		printf("SQLite!\n");
	} else {
		emsg("could not identify given database\n");
		err = 1;
	}
	fclose(file);
	return err;
}

/*Calculate approriate itag(s) based on configuration*/
static int* calculateitag(const char *format)
{
	/*For DASH streams (3 digits), we need a seperate audio stream
	  Video stream first, then audio
	  Prefer non-DASH streams (2 digits) to avoid overhead
	  -1 is when no stream is available/necessary*/
	char *choice;
	int *itag = (int *)malloc(2 * sizeof(int));
	if(format[0] == '\0') {
		choice = strdup(config.format);
	} else {
		choice = strdup(format);
	}
	switch(config.res) {
		case 144:
			if(!strcmp(choice, "webm")) {
				itag[0] = 278;
				itag[1] = 171;
			} else if(!strcmp(choice, "3gp")) {
				itag[0] = 17;
				itag[1] = -1;
			} else if(!strcmp(choice, "mp4")) {
				itag[0] = 160;
				itag[1] = 139;
			}
			break;
		case 240:
			if(!strcmp(choice, "webm")) {
				itag[0] = 242;
				itag[1] = 249;
			} else if(!strcmp(choice, "3gp")) {
				itag[0] = 36;
				itag[1] = -1;
			} else if(!strcmp(choice, "mp4")) {
				itag[0] = 133;
				itag[1] = 139;
			}
			break;
		case 360:
			if(!strcmp(choice, "webm")) {
				itag[0] = 43;
				itag[1] = -1;
			} else if(!strcmp(choice, "mp4")) {
				itag[0] = 18;
				itag[1] = -1;
			}
			break;
		case 480:
			if(!strcmp(choice, "webm")) {
				itag[0] = 244;
				itag[1] = 251;
			} else if(!strcmp(choice, "mp4")) {
				itag[0] = 135;
				itag[1] = 139;
			}
			break;
		case 720:
			if(!strcmp(choice, "webm")) {
				itag[0] = 247;
				itag[1] = 251;
			} else if(!strcmp(choice, "mp4")) {
				itag[0] = 22;
				itag[1] = -1;
			}
			break;
		case 1080:
			if(!strcmp(choice, "webm")) {
				itag[0] = 248;
				itag[1] = 251;
			} else if(!strcmp(choice, "mp4")) {
				itag[0] = 137;
				itag[1] = 140;
			}
			break;
		case 1440:
			if(!strcmp(choice, "webm")) {
				itag[0] = 271;
				itag[1] = 251;
			} else if(!strcmp(choice, "mp4")) {
				itag[0] = 264;
				itag[1] = 140;
			}
			break;
		case 2160:
			if(!strcmp(choice, "webm")) {
				itag[0] = 313;
				itag[1] = 251;
			} else if(!strcmp(choice, "mp4")) {
				itag[0] = 266;
				itag[1] = 140;
			}
			break;
	}
	free(choice);
	return itag;
}

/*Get raw video URL(s) for a given youtube video with VID vid*/
char** getyoutubestream(const char *vid)
{
	char *term = NULL, *next, *url, *html;
	char **raw = (char **)calloc(2, sizeof(char));
	int i = 0, j = 0, *itag;
	long int found = 0;
	url = (char *)malloc(33 + strlen(vid));
	strcpy(url, "https://www.youtube.com/watch?v=");
	strcat(url, vid);
	if(!(html = gethtml(url, NULL))) {
		return raw;
	}
	free(url);
	itag = calculateitag("");
	if(itag[0] == -1) {
		free(itag);
		free(html);
		return raw;
	} else if(itag[0] >= 100) {
		term = strdup("\"adaptive_fmts\"");
	} else {
		term = strdup("\"url_encoded_fmt_stream_map\"");
	}
	do {
		raw[i] = strdup(strstr(html, term));
		do {
			raw[i] = strstr(raw[i], "url=") + 4;
			next = strstr(raw[i], "itag=");
			if(next) {
				next += 5;
				found = strtol(next, &next, 10);
			}
		} while(next && found != LONG_MIN && \
			found != LONG_MAX && found != itag[i]);
		if(!next || found == LONG_MIN || found == LONG_MAX) {
			if(j) {
				raw[0] = NULL;
				raw[1] = NULL;
				break;
			}
			emsg("configured format not found, falling back to ");
			if(!strcmp(config.format, "webm")) {
				itag = calculateitag("mp4");
				fprintf(stderr, "mp4...\n");
			} else {
				itag = calculateitag("webm");
				fprintf(stderr, "webm...\n");
			}
			j = 1;
			i = 0;
			continue;
		}
		raw[i] = strtok(raw[i], ",;\\");
		i++;
	} while(i < 2 && itag[i] != -1);
	free(html);
	free(itag);
	free(term);
	return raw;
}

#ifndef NOTUI
/*Check if we have a thumbnail, and if not, download it*/
/* Returns the image filename if it exists*/
char* getimage(const char *id, const char *thumburl)
{
	CURL *curl;
	CURLcode res;
	FILE *file;
	char *filename = NULL, *path = NULL;
	if(!id || !thumburl) {
		return NULL;
	}
	filename = (char *)malloc(strlen(id) + 5);
	sprintf(filename, "%s.jpg", id);
	path = (char *)malloc(strlen(config.thumbs) + strlen(id) + 5);
	sprintf(path, "%s/%s.jpg", config.thumbs, id);
	if((file = fopen(path, "r"))) {
		/*It already exists, success*/
		fclose(file);
		free(path);
		return filename;
	}

	/*Now try to download it*/
	curl = curl_easy_init();
	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlFile);
	curlSetup(curl);
	file = fopen(path, "wb");
	if(file) {
		curl_easy_setopt(curl, CURLOPT_URL, thumburl);
		curl_easy_setopt(curl, CURLOPT_WRITEDATA, file);
		res = curl_easy_perform(curl);
		if(res != CURLE_OK) {
			emsg("network connection failed - %s\n",
			     curl_easy_strerror(res));
			fclose(file);
			free(path);
			return NULL;
		}
		free(path);
		fclose(file);
		return filename;
	}
	free(path);
	return NULL;
}
#endif /*NOTUI*/

/*Update video listing for every subscription*/
int syncvids(void)
{
	achannel *chan = NULL;
	int i = 0;
	int err = 0;
	size_t max = 0;
	int subs = searchchannels(NULL, &chan);
	if(subs == -1) {
		return 1;
	}
	BLU
	BOLD
	printf(":: ");
	WHT
	printf(_("Synchronising subscription database...\n"));
	RESET
	while(chan[i].name) {
		if(strlen(chan[i].name) > max) {
			max = strlen(chan[i].name);
		}
		i++;
	}
	i = 0;
	while(chan[i].name) {
		int vids = 0;
		char *cid = geturlid(chan[i].url);
		vids = updvidlist(cid);
#ifndef NOTUI
		if(config.tshow) {
			getimage(cid, chan[i].thumb);
		}
#endif
		if(vids == -1) {
			err = 1;
			continue;
		}
		printf(" %s%*i\n", chan[i].name,
		       (int)(max-strlen(chan[i].name)+2), vids);
		freechannel(chan[i]);
		i++;
	}
	free(chan);
	printf(_("All channel listings updated.\n"));
	return err;
}

/*Parse config options, gets called for each name/value pair*/
static int parseconfig(void *user, const char *section,
		       const char *name, const char *value)
{
	aconf *pconfig = (aconf*)user;
	#define MATCH(s, n) strcmp(section, s) == 0 && strcmp(name, n) == 0
	if(MATCH("playing", "player")) {
		if(!strcmp(value, "none") || !strcmp(value, "mpv") ||
		   !strcmp(value, "mplayer") || !strcmp(value, "vlc")) {
			pconfig->player = strdup(value);
		} else {
			emsg("invalid player in config file.\n");
			return 0;
		}
	} else if(MATCH("playing", "format")) {
		if(!strcmp(value, "webm") || !strcmp(value, "mp4") ||
		   !strcmp(value, "3gp")) {
			pconfig->format = strdup(value);
		} else {
			emsg("invalid format in config file.\n");
			return 0;
		}
	} else if(MATCH("playing", "resolution")) {
		long n = strtol(value, NULL, 10);
		if(n != 144 && n != 240 && n != 360 && n != 480 &&
		   n != 720 && n != 1080 && n != 1440 && n != 2160) {
			emsg("invalid resolution in config file.\n");
			return 0;
		} else {
			pconfig->res = n;
		}
	} else if(MATCH("proxy", "type")) {
		if(!strcmp(value, "HTTP")) {
			pconfig->proxy = CURLPROXY_HTTP;
		} else if(!strcmp(value, "HTTPS")) {
			pconfig->proxy = CURLPROXY_HTTPS;
		} else if(!strcmp(value, "SOCKS4")) {
			pconfig->proxy = CURLPROXY_SOCKS4;
		} else if(!strcmp(value, "SOCKS5")) {
			pconfig->proxy = CURLPROXY_SOCKS5;
		} else {
			emsg("invalid proxy type in config file.\n");
			return 0;
		}
	} else if(MATCH("proxy", "ip")) {
		pconfig->ip = strdup(value);
	} else if(MATCH("proxy", "port")) {
		pconfig->port = strtol(value, NULL, 10);
	} else if(MATCH("display", "list")) {
		pconfig->list = strtol(value, NULL, 10);
	} else if(MATCH("display", "sort")) {
		if(!strcmp(value, "newest") || !strcmp(value, "oldest")) {
			pconfig->sort = strdup(value);
		} else {
			emsg("invalid value in config file: sort.\n");
			return 0;
		}
	} else if(MATCH("display", "colour")) {
		if(!strcmp(value, "on")) {pconfig->colour = 1;}
		else if(!strcmp(value, "off")) {
			pconfig->colour = 0;
		} else {
			emsg("invalid value in config file: colour.\n");
			return 0;
		}
	} else if(MATCH("files", "subs")) {
		pconfig->subs = strdup(value);
	} else if(MATCH("files", "videos")) {
		pconfig->vids = strdup(value);
	} else if(MATCH("files", "thumbnails")) {
		pconfig->thumbs = strdup(value);
	} else if(MATCH("thumbnails", "show")) {
		if(!strcmp(value, "on")) {
			pconfig->tshow = 1;
		} else if(!strcmp(value, "off")) {
			pconfig->tshow = 0;
		} else {
			emsg("invalid value in config file: show.\n");
			return 0;
		}
	} else if(MATCH("thumbnails", "save")) {
		if(!strcmp(value, "on")) {
			pconfig->tsave = 1;
		} else if(!strcmp(value, "off")) {
			pconfig->tsave = 0;
		} else {
			emsg("invalid value in config file: save.\n");
			return 0;
		}
	} else if(MATCH("other", "vim mode")) {
		if(!strcmp(value, "on")) {
			pconfig->vim = 1;
		} else if(!strcmp(value, "off")) {
			pconfig->vim = 0;
		} else {
			emsg("invalid value in config file: vim mode.\n");
			return 0;
		}
	} else if(MATCH("other", "encryption")) {
		if(!strcmp(value, "on")) {
			pconfig->enc = 1;
		} else if(!strcmp(value, "off")) {
			pconfig->enc = 0;
		} else {
			emsg("invalid value in config file: encryption.\n");
			return 0;
		}
	} else if(MATCH("other", "verbose")) {
		if(!strcmp(value, "on")) {
			pconfig->verbose = 1;
		} else if(!strcmp(value, "off") && !config.verbose) {
			pconfig->verbose = 0;
		} else if(strcmp(value, "off")) {
			emsg("invalid value in config file: verbose.\n");
			return 0;
		}
	} else {
		emsg("invalid config file.\n");
	}
	return 1; /*handler function returns nonzero on success*/
}

/*Return the username of the current user*/
static char* currentuser(void)
{
#ifdef HAVE_PWD_H
	struct passwd *p = getpwuid(getuid());
	if(p) {
		return p->pw_name;
	}
#elif HAVE_WINDOWS_H
	char *user = NULL;
	long unsigned int charcount;
	if(GetUserName(user, &charcount)) {
		return user;
	}
#endif
	return getenv(USERVAR);;
}

/*Return the config file location*/
static char* getconfiglocation(void)
{
	char *file;
	if(getenv(CONFIGVAR)) {
		file = (char *)malloc(strlen(getenv(CONFIGVAR)) + 32);
		strcpy(file, getenv(CONFIGVAR));
	} else {
		char *user = currentuser();
		if(!user) {
			emsg("could not find username\n");
			return NULL;
		}
		file = (char *)malloc(64);
#ifndef _WIN32
		if(!strcmp(user, "root")) {
			sprintf(file, "/etc");
		} else {
			sprintf(file, "/home/%s/.config", user);
		}
#else
		sprintf(file, "C:/Users/%s/AppData", user);
#endif
	}
	strcat(file, "/tubeman");
	mkdir(file, 0744);
	strcat(file, "/config");
	verbose(_("Using confguration at: %s\n"), file);
	return file;
}

/*Find and then parse the configuration file*/
static int getconfig(char *filename)
{
	if(!filename) {
		filename = getconfiglocation();
	}
	if(ini_parse(filename, parseconfig, &config) < 0) {
		emsg("can't load config file\n");
		fprintf(stderr, _("Use the --gen-config option \
to create the default config file.\n"));
		return 1;
	}
	free(filename);
	return 0;
}

/*Generate default config file by writing the data in string array to file*/
static int writeconfig(char *filename)
{
	int i = 0;
	FILE *defaultfile;
	if(!filename) {
		filename = getconfiglocation();
	}
	defaultfile = fopen(filename, "w");
	if(!defaultfile) {
		emsg("could not write to file - %s\n", filename);
		free(filename);
		return 1;
	}
	printf(_("Generating default config file in %s\n"), filename);
	printf(_("Note this will overwrite any existing config.\n"));
	free(filename);
	do {
		fprintf(defaultfile, "%s", defaultconfig[i]);
		i++;
	} while(strcmp(defaultconfig[i], ""));
	fclose(defaultfile);
	return 0;
}

/*Find locations of databases and open them*/
static int initdatabases(void)
{
	char *errmsg;
	int err;
	if(!config.subs || !config.vids || !config.thumbs) {
		char *dir;
		if(getenv(DATAVAR)) {
			dir = (char *)malloc(strlen(getenv(DATAVAR)) + 10);
			strcpy(dir, getenv(DATAVAR));
		} else {
			char *user = currentuser();
			if(!user) {
				emsg("could not find username\n");
				return 1;
			}
			dir = (char *)malloc(64);
#ifndef _WIN32
			if(!strcmp(user, "root")) {
				sprintf(dir, "/etc");
			} else {
				sprintf(dir, "/home/%s/.local/share", user);
			}
#else
			sprintf(dir, "C:/Users/%s/AppData", user);
#endif
		}
		strcat(dir, "/tubeman/");
		mkdir(dir, 0744);
		if(!config.subs) {
			config.subs = (char *)malloc(strlen(dir) + 18);
			strcpy(config.subs, dir);
			strcat(config.subs, "subscriptions.db");
		}
		if(!config.vids) {
			config.vids = (char *)malloc(strlen(dir) + 10);
			strcpy(config.vids, dir);
			strcat(config.vids, "videos.db");
		}
		if(!config.thumbs && config.tshow) {
			config.thumbs = (char *)malloc(strlen(dir) + 11);
			strcpy(config.thumbs, dir);
			strcat(config.thumbs, "thumbnails");
			mkdir(config.thumbs, 0744);
		}
		free(dir);
	}
	err = sqlite3_open(config.subs, &subdb);
	if(err != SQLITE_OK) {
		emsg("could not open subscription database - %s\n",
		     sqlite3_errmsg(subdb));
		return 1;
	}
	err = sqlite3_open(config.vids, &viddb);
	if(err != SQLITE_OK) {
		emsg("could not open video database - %s\n",
		     sqlite3_errmsg(viddb));
		return 1;
	}
	free(config.vids);
	free(config.subs);
	sqlite3_exec(subdb, " \
CREATE TABLE IF NOT EXISTS subscriptions ( \
uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, \
service_id INTEGER NOT NULL, \
url TEXT, \
name TEXT, \
avatar_url TEXT, \
subscriber_count INTEGER, \
description TEXT);", \
			NULL, NULL, &errmsg);
	return 0;
}

/*Run at exit: close databases, free memory and clean up libcurl*/
static void tidyup(void)
{
	/*will need to delete thumbnails here if saving is disabled*/
	sqlite3_close(subdb);
	sqlite3_close(viddb);
	free(config.player);
	free(config.format);
	free(config.ip);
	free(config.sort);
	free(config.thumbs);
	curl_global_cleanup();
}

/*Play given Youtube URL using the configured player*/
/* Setting audio=1 disables video*/
static int playvideo(const char *url, const int audio)
{
	char *vid, **stream, *argv[12];
	int err, argc;
	vid = geturlid(url);
	if(!vid) {
		emsg("could not find video ID\n");
		return 1;
	}
	argc = 0;
	if(!strcmp(config.player, "none")) {
		return 1;
	} else if(!strcmp(config.player, "ffplay")) {
		argv[0] = strdup("ffplay");
		argv[1] = strdup("-fs");
		argv[2] = strdup("-v");
		argv[3] = strdup("16");
		argc += 4;
		if(audio) {
			argv[4] = strdup("-vn");
			argc += 1;
		}
	} else if(!strcmp(config.player, "mpv")) {
		argv[0] = strdup("mpv");
		argv[1] = strdup("-fs");
		argv[2] = strdup("--really-quiet");
		argc += 3;
		if(audio) {
			argv[3] = strdup("--vid=no");
			argc += 1;
		}
	} else if(!strcmp(config.player, "mplayer")) {
		argv[0] = strdup("mplayer");
		argv[1] = strdup("-fs");
		argv[2] = strdup("-really-quiet");
		argc += 3;
		if(audio) {
			argv[3] = strdup("-vo");
			argv[4] = strdup("null");
			argv[5] = strdup("-vc");
			argv[6] = strdup("null");
			argc += 4;
		}
	} else if(!strcmp(config.player, "vlc")) {
		argv[0] = strdup("vlc");
		argv[1] = strdup("-f");
		argv[2] = strdup("-q");
		argc += 3;
		if(audio) {
			argv[3] = strdup("--no-video");
			argc += 1;
		}
	}
	stream = getyoutubestream(vid);
	if(stream[0] && ((audio && !stream[1]) || !audio)) {
		argv[argc] = strdup(stream[0]);
		argc++;
	} else if(!audio) {
		emsg("no matching video stream found\n");
		curl_free(stream);
		return 1;
	}
	if(stream[1]) {
		if(!audio && !strcmp(config.player, "ffplay")) {
			
		} else if(!audio && !strcmp(config.player, "mpv")) {
			argv[argc] = strdup("-audio-file");
			argc++;
		} else if(!audio && !strcmp(config.player, "mplayer")) {
			argv[argc] = strdup("-audiofile");
			argc++;
		} else if(!audio && !strcmp(config.player, "vlc")) {
			argv[argc] = strdup("--input-slave");
			argc++;
		}
		argv[argc] = strdup(stream[1]);
		argc++;
	} else if(audio) {
		emsg("no matching audio stream found\n");
		curl_free(stream);
		return 1;
	}
	argv[argc] = NULL;
	err = execprog(argc, argv, NULL);
	freeargs(argc, argv);
	curl_free(stream);
	return err;
}

/*Download video at given Youtube URL
  If dest is not NULL, saves there, otherwise uses current directory*/
int downloadvideo(const char *url, const char *dest)
{
	CURL *curl;
	CURLcode res;
	FILE *file;
	char **stream, *filename;
	char *vid = geturlid(url);
	if(!vid) {
		emsg("could not find video ID\n");
		return 1;
	}
	stream = getyoutubestream(vid);
	if(dest) {
		filename = (char *)malloc(strlen(dest) + strlen(vid) + 6);
		sprintf(filename, "%s/%s", dest, vid);
	} else {
		filename = (char *)malloc(strlen(vid) + 6);
		strcpy(filename, vid);
	}
	if(strstr(stream[0], "webm")) {
		strcat(filename, ".webm");
	} else if(strstr(stream[0], "mp4")) {
		strcat(filename, ".mp4");
	} else if(strstr(stream[0], "3gp")) {
		strcat(filename, ".3gp");
	}
	curl = curl_easy_init();
	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlFile);
	curlSetup(curl);
	file = fopen(filename, "wb");
	if(file) {
		curl_easy_setopt(curl, CURLOPT_URL, stream[0]);
		curl_easy_setopt(curl, CURLOPT_WRITEDATA, file);
		res = curl_easy_perform(curl);
		if(res != CURLE_OK) {
			emsg("network connection failed - %s\n",
			     curl_easy_strerror(res));
			free(vid);
			fclose(file);
			curl_free(stream);
			free(filename);
			return 1;
		}
		fclose(file);
	}
	if(strcmp(stream[1], "")) { /*Get audio stream if necessary*/
		strcat(filename, "-audio");
		file = fopen(filename, "wb");
		if(file) {
			curl_easy_setopt(curl, CURLOPT_URL, stream[1]);
			curl_easy_setopt(curl, CURLOPT_WRITEDATA, file);
			res = curl_easy_perform(curl);
			if(res != CURLE_OK) {
				emsg("network connection failed - %s\n",
				     curl_easy_strerror(res));
				free(vid);
				fclose(file);
				curl_free(stream);
				free(filename);
				return 1;
			}
			fclose(file);
		}
	}
	curl_easy_cleanup(curl);
	free(filename);
	curl_free(stream);
	return 0;
}

/*Print help text to standard output*/
static int showhelp(const int mode)
{
	#define DESC "A command line YouTube subscription manager and browser"
	if(mode == -1) {
		printf(_("Usage: tubeman [option] [...]\n"));
		printf(_("  or: tubeman -S [option] [...]\n"));
		printf(_("  or: tubeman -R [option] [...]\n"));
		printf(_("  or: tubeman -Q [option] [...]\n"));
		printf(_("  or: tubeman -F [option] [...]\n"));
		printf(_(DESC "\n\n"));
		printf(_("Options:\n"));
		printf(_("  -p, --play <URL>        "));
		printf(_("play video with given URL\n"));
		printf(_("  -i, --import <file>     "));
		printf(_("import subscriptions from database or OPML file\n"));
		printf(_("  -v, --verbose           "));
		printf(_("verbose mode\n"));
		printf(_("  -h, --help              "));
		printf(_("show help text\n"));
		printf(_("  -V, --version           "));
		printf(_("show version\n\n"));
		printf(_("Modes:\n"));
		printf(_("  -S, --sync [options]    "));
		printf(_("add or update channels\n"));
		printf(_("  -R, --remove [options]  "));
		printf(_("remove subscriptions\n"));
		printf(_("  -Q, --query [options]   "));
		printf(_("query local subscriptions\n"));
		printf(_("  -F, --files [options]   "));
		printf(_("query video listings\n\n"));
		printf(_("use 'tubeman {-h,--help}' "));
		printf(_("followed by a mode for available options\n"));
	} else if(mode == 0) {
		printf(_("Usage: tubeman {-S,--sync} [option] [...]\n"));
		printf(_(DESC "\n\n"));
		printf(_("Options:\n"));
		printf(_("  URL(s)                  "));
		printf(_("subscribe to channels at the given URLs\n"));
		printf(_("  -y, --refresh           "));
		printf(_("update subscriptions and video listings\n"));
		printf(_("  -s, --search ...        "));
		printf(_("search for a channel on Youtube\n"));
		printf(_("  -i, --info URL          "));
		printf(_("show info of a given Youtube channel\n"));
	} else if(mode == 1) {
		printf(_("Usage: tubeman {-R,--remove} [ID, ID...]\n"));
		printf(_(DESC "\n\n"));
		printf(_("Options:\n"));
		printf(_("  ID(s)                   "));
		printf(_("unsubscribe from channels with given ID(s)\n"));
	} else if(mode == 2) {
		printf(_("Usage: tubeman {-Q,--query} [option] [...]\n"));
		printf(_(DESC "\n\n"));
		printf(_("Options:\n"));
		printf(_("  -s, --search ...        "));
		printf(_("search for a channel in local subscriptions\n"));
		printf(_("  -i, --info ID           "));
		printf(_("show information of a subscribed channel\n"));
	} else if(mode == 3) {
		printf(_("Usage: tubeman {-F,--file} [option] [...]\n"));
		printf(_(DESC "\n\n"));
		printf(_("Options:\n"));
		printf(_("  -s, --search ...        "));
		printf(_("search for a video in local database\n"));
		printf(_("  -l, --list ID           "));
		printf(_("list a subscribed channel's videos\n"));
		printf(_("  -i, --info ID           "));
		printf(_("show a video's information\n"));
		printf(_("  -d, --download URL      "));
		printf(_("download a video\n"));
	} else {
		emsg("invalid help mode: %i\n", mode);
		return 1;
	}
	printf("\n");
	printf(_("Report bugs to: <" PACKAGE_BUGREPORT ">\n"));
	printf(_(PACKAGE_NAME " homepage: <" PACKAGE_URL ">\n"));
	return 0;
}

/*--version output*/
static void showversion(void)
{
	printf(PACKAGE_NAME " " PACKAGE_VERSION "\n");
	printf(_("Compiled with: "));
	#ifdef __GNUC__
		printf("GCC %i.%i", __GNUC__, __GNUC_MINOR__);
	#elif defined(__clang__)
		printf("Clang %i.%i", __clang_major__, __clang_minor__);
	#elif defined(__MINGW32__)
		printf("MinGW %i.%i", __MINGW32_MAJOR_VERSION,
		                      __MINGW32_MINOR_VERSION);
	#else
		printf(_("Unknown"));
	#endif
	printf("\n\nCopyright (C) 2018 Oliver Galvin\n");
	printf(_("License GPLv3+: GNU GPL version 3 or later "));
	printf("<http://gnu.org/licenses/gpl.html>\n");
	printf(_("This is free software: you are free to change and "));
	printf(_("redistribute it.\n"));
	printf(_("There is NO WARRANTY, to the extent permitted by law.\n"));
}

#ifndef NOTUI
/*Show an image in the terminal with w3mimgdisplay*/
static int showimage(const char *imgpath,
		     const int height, const int width,
		     const int y, const int x)
{
	int err = 0;
	char *libpath[2] = { NULL, NULL };
	char *cmd = (char *)malloc(strlen(imgpath) + 50);
	sprintf(cmd, "0;1;%i;%i;%i;%i;;;;;%s\n4;\n3;",
		x, y, width, height, imgpath);
	libpath[0] = strdup("/usr/lib/w3m/w3mimgdisplay");
	err = execprog(1, libpath, cmd);
	free(cmd);
	free(libpath[0]);
	return err;
}

/*Show an image in the terminal with w3mimgdisplay*/
static int clearimage(const int height, const int width,
		      const int y, const int x)
{
	int err = 0;
	char *libpath[2] = { NULL, NULL };
	char *cmd = (char *)malloc(50);
	sprintf(cmd, "6;%i;%i;%i;%i\n4;\n3;", x + 2, y + 2, width, height);
	libpath[0] = strdup("/usr/lib/w3m/w3mimgdisplay");
	err = execprog(1, libpath, cmd);
	free(cmd);
	free(libpath[0]);
	return err;
}

/*Get resolution of current window*/
static void getres(int *maxheight, int *maxwidth)
{
#ifdef HAVE_SYS_IOCTL_H
	struct winsize w;
	if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &w)) {
		maxheight = 0;
		maxwidth = 0;
	} else {
		*maxheight = w.ws_ypixel;
		*maxwidth = w.ws_xpixel;
	}
#elif defined(HAVE_WINDOWS_H)
	RECT w;
	HWND window = GetActiveWindow();
	GetWindowRect(&window, &desktop);
	width = w.right;
	height = w.bottom;
#endif /*HAVE_SYS_IOCTL_H*/
}
#endif /*NOTUI*/

#ifndef NOTUI
/*TUI Subscription menu*/
static void tui_subs(int row, int col, int maxh, int maxw)
{
	int c = 0;
	int i = 0;
	int err = 0;
	WINDOW *subwin;
	MENU *submenu;
	ITEM **sublist;
	achannel *chan = NULL;
	int subs = searchchannels(NULL, &chan);
	if(subs == -1) {
		return;
	}
	sublist = (ITEM **)calloc((size_t)(subs + 1), sizeof(ITEM *));
	while(chan[i].name) {
		sublist[i] = new_item(chan[i].name, chan[i].desc);
		i++;
	}
	sublist[i] = (ITEM *)NULL;
	submenu = new_menu((ITEM **)sublist);
	menu_opts_off(submenu, O_NONCYCLIC);
	subwin = newwin(row - 2, col/2, 1, 2);
	keypad(subwin, TRUE);
	mvwprintw(subwin, 1, 4, "Your Subscriptions:");
	set_menu_win(submenu, subwin);
	set_menu_sub(submenu, derwin(subwin, row - 6, col/2 - 4, 3, 2));
	set_menu_format(submenu, row - 4, 0);
	post_menu(submenu);
	box(subwin, 0, 0);
	i = 0;
	while(c != 'q' && c != 'h' && c != KEY_LEFT) {
		if(config.tshow) {
			char *img = getimage(geturlid(chan[i].url),
						      chan[i].thumb);
			err = chdir(config.thumbs);
			if(img && !err) {
				refresh();
				if(maxh && maxw) {
					showimage(img, 300, 300,
					    maxh/2 - 150, maxw/2 + 150);
				} else {
					showimage(img, 300, 300,
					    14*(row/2) - 150, 8*(col/2) + 150);
				}
			}
		}
		wrefresh(subwin);
		c = wgetch(subwin);
		if(c == KEY_DOWN || c == 'j') {
			menu_driver(submenu, REQ_DOWN_ITEM);
		} else if (c == KEY_UP || c == 'k') {
			menu_driver(submenu, REQ_UP_ITEM);
		} else if (c == KEY_PPAGE) {
			menu_driver(submenu, REQ_SCR_UPAGE);
		} else if (c == KEY_NPAGE) {
			menu_driver(submenu, REQ_SCR_DPAGE);
		} else if (c == KEY_HOME) {
			menu_driver(submenu, REQ_FIRST_ITEM);
		} else if (c == KEY_END) {
			menu_driver(submenu, REQ_LAST_ITEM);
		}
		i = item_index(current_item(submenu));
	}
	unpost_menu(submenu);
	free_menu(submenu);
	for(i=0; i<subs; i++) {
		free_item(sublist[i]);
		freechannel(chan[i]);
	}
	werase(subwin);
	wrefresh(subwin);
	delwin(subwin);
	free(chan);
}

/*TUI About screen*/
static void tui_about(int col)
{
	int c;
	WINDOW *about = newwin(12, 80, 10, col/2 - 40);
	keypad(about, TRUE);
	mvwprintw(about, 1, 1, PACKAGE_NAME " " PACKAGE_VERSION "\n");
	mvwprintw(about, 2, 1,
	_("A local Youtube subsciption manager and browser."));
	mvwprintw(about, 3, 1, _("Compiled with: "));
	#ifdef __GNUC__
		mvwprintw(about, 3, 16, "GCC %i.%i",  __GNUC__,
					       __GNUC_MINOR__);
	#elif defined(__clang__)
		mvwprintw(about, 3, 16, "Clang %i.%i", __clang_major__,
						      __clang_minor__);
	#elif defined(__MINGW32__)
		mvwprintw(about, 3, 16, "MinGW %i.%i",
		__MINGW32_MAJOR_VERSION, __MINGW32_MINOR_VERSION);
	#else
		mvwprintw(about, 3, 16, _("Unknown"));
	#endif
	mvwprintw(about, 4, 1, "Copyright (C) 2018 Oliver Galvin");
	mvwprintw(about, 5, 1,
		_("License GPLv3+: GNU GPL version 3 or later "));
	mvwprintw(about, 5, 44, "<http://gnu.org/licenses/gpl.html>");
	mvwprintw(about, 7, 1,
	_("This is free software: \
you are free to change and redistribute it."));
	mvwprintw(about, 8, 1,
	_("There is NO WARRANTY, to the extent permitted by law."));
	box(about, 0, 0);
	wrefresh(about);
	c = 0;
	while(c != 'q' && c != 'h' && c != KEY_LEFT) {
		c = wgetch(about);
		wrefresh(about);
	}
	werase(about);
	wrefresh(about);
	delwin(about);
}
#endif /*NOTUI*/

#ifndef NOTUI
/*Interactive mode - a curses based TUI*/
static int tui(const char *configpath)
{
	int err = 0;
	int row, col;
	int i;
	int c = 0;
	ITEM **list;
	MENU *menu;
	WINDOW *mainwindow;
	const int n = 7; /*Number of options*/
	const char *choices[] = {" Show latest videos  ",
				 "Show trending videos ",
				 "   Search Youtube    ",
				 " List subscriptions  ",
				 " Edit configuration  ",
				 "        About        ",
				 "        Quit         "
				};
	int maxwidth = 0, maxheight = 0;
	getres(&maxheight, &maxwidth);
	initscr();
	getmaxyx(stdscr, row, col);
	if(row < 20 || col < 80) {
		endwin();
		return 1;
	}
	cbreak();
	noecho();
	keypad(stdscr, TRUE);
	list = (ITEM **)calloc((size_t)(n + 1), sizeof(ITEM *));
	for(i=0; i<n; i++) {
		list[i] = new_item(choices[i], "");
	}
	list[n] = (ITEM *)NULL;
	menu = new_menu((ITEM **)list);
	menu_opts_off(menu, O_NONCYCLIC);
	menu_opts_off(menu, O_SHOWDESC);
	mainwindow = newwin(n + 2, 24, 10, col/2 - 12);
	keypad(mainwindow, TRUE);
	set_menu_win(menu, mainwindow);
	set_menu_sub(menu, derwin(mainwindow, n + 1, 23, 1, 1));
	post_menu(menu);
	while(c != 'q' && c != 'h' && c != KEY_LEFT) {
		mvprintw(3, col/2-21, " _______    _          __  __             ");
		mvprintw(4, col/2-21, "|__   __|  | |        |  \\/  |           ");
		mvprintw(5, col/2-21, "   | |_   _| |__   ___| \\  / | __ _ _ __ ");
		mvprintw(6, col/2-21, "   | | | | | '_ \\ / _ \\ |\\/| |/ _' | '_ \\");
		mvprintw(7, col/2-21, "   | | |_| | |_) |  __/ |  | | (_| | | | |");
		mvprintw(8, col/2-21, "   |_|\\__,_|_.__/ \\___|_|  |_|\\__,_|_| |_|");
		refresh();
		wrefresh(mainwindow);
		c = wgetch(mainwindow);
		if(c == KEY_DOWN || c == 'j') {
			menu_driver(menu, REQ_DOWN_ITEM);
		} else if (c == KEY_UP || c == 'k') {
			menu_driver(menu, REQ_UP_ITEM);
		} else if (c == KEY_HOME || c == KEY_PPAGE) {
			menu_driver(menu, REQ_FIRST_ITEM);
		} else if (c == KEY_END || c == KEY_NPAGE) {
			menu_driver(menu, REQ_LAST_ITEM);
		} else if (c == KEY_RIGHT || c == 'l' || c == 10) {
			unpost_menu(menu);
			wrefresh(mainwindow);
			i = item_index(current_item(menu));
			if(i == 0) {
				
			} else if(i == 1) {
				
			} else if(i == 2) {
				
			} else if(i == 3) {
				tui_subs(row, col, maxheight, maxwidth);
			} else if(i == 4) {
				char *argv[3] = { NULL, NULL, NULL };
				argv[0] = strdup(getenv("EDITOR"));
				if(!argv[0]) {
					argv[0] = strdup("vi");
				}
				if(!configpath) {
					argv[1] = strdup(getconfiglocation());
				} else {
					argv[1] = strdup(configpath);
				}
				err = execprog(2, argv, NULL);
				erase();
				werase(mainwindow);
			} else if(i == 5) {
				tui_about(col);
			} else if(i == 6) {
				c = 'q';
			}
			erase();
			refresh();
			post_menu(menu);
		}
	}
	unpost_menu(menu);
	free_menu(menu);
	for(i=0; i<=n; i++) {
		free_item(list[i]);
	}
	delwin(mainwindow);
	endwin();
	return err;
}
#endif /*NOTUI*/

/*Parse pacman-style command line options, and initialise*/
int main(int argc, char *argv[])
{
	/*On start: find cache dir*/
	int mode = -1;
	int n = 0;
	int option_index = 0;
	int i, err = EXIT_SUCCESS;
	int opt;
	char *file = NULL;
	#define OPTNUM 16
	const char *rawlist = "SRQFylisdcpIghVv";
	const char *optlist = ":S:R:QFyl:i:s:d:c:p:I:ghVv";
	int options[OPTNUM];
	char *args[OPTNUM];
	const struct option long_options[OPTNUM + 1] = {
		/*Mode options (upper case short options)*/
		{"sync",       required_argument, 0, 0},
		{"remove",     required_argument, 0, 1},
		{"query",      no_argument,       0, 2},
		{"file",       no_argument,       0, 3},

		/*Options for modes*/
		{"refresh",    no_argument,       0, 4},
		{"list",       required_argument, 0, 5},
		{"info",       required_argument, 0, 6},
		{"search",     required_argument, 0, 7},
		{"download",   required_argument, 0, 8},

		/*Other options*/
		{"config",     required_argument, 0, 9},
		{"play",       required_argument, 0, 10},
		{"import",     required_argument, 0, 11},
		{"gen-config", no_argument,       0, 12},
		{"help",       no_argument,       0, 13},
		{"version",    no_argument,       0, 14},
		{"verbose",    no_argument,       0, 15},
		{0, 0, 0, 0}
	};
	/*Initialise arguments*/
	for(i=0; i<OPTNUM; i++) {
		args[i] = NULL;
		options[i] = 0;
	}
	while((opt = getopt_long(argc, argv, optlist, \
	    long_options, &option_index)) != -1) {
		n++;
		if(opt == '?') {
			emsg("invalid option\n");
			return EXIT_FAILURE;
		} else if(opt == ':') {
			if(!(optopt == 's' && options[2]) && \
				!((optopt == 'S' || optopt == 'R' || \
				optopt == 0 || optopt == 1) && options[13])) {
				emsg("missing argument\n");
				freeargs(OPTNUM, args);
				return EXIT_FAILURE;
			} else {
				opt = (int)optopt;
			}
		}
		if(opt > OPTNUM) {
			opt = (int)(strchr(rawlist, opt) - rawlist);
		}
		if(opt == 0 || opt == 1 || opt == 2 || opt == 3) {
			if(mode >= 0) {
				emsg("only one mode allowed\n");
				freeargs(OPTNUM, args);
				return EXIT_FAILURE;
			}
			mode = opt;
		}
		options[opt] = 1;
		if(optarg) {
			if(options[0] && *optarg == 'y') {
				options[4] = 1;
				n++;
			} else if(optarg) {
				args[opt] = strdup(optarg);
			}
		}
	}
	if(options[9]) {		/*Manually set config location first*/
		n -= 1;
		file = strdup(args[8]);
	}
	if(options[15]) {		/*Verbose mode*/
		n -= 1;
		config.verbose = 1;
	}
	if(!options[12] && !options[13] && !options[14]) {
		/*Initialisation -
		only when --gen-config, --version and --help aren't used*/
		if(getconfig(file)) {
			if(options[9]) {free(file);}
			freeargs(OPTNUM, args);
			return EXIT_FAILURE;
		}
		if(curl_global_init(CURL_GLOBAL_DEFAULT)) {
			emsg("could not initialise libcurl\n");
			if(options[9]) {free(file);}
			freeargs(OPTNUM, args);
			return EXIT_FAILURE;
		}
		if(initdatabases()) {
			freeargs(OPTNUM, args);
			return EXIT_FAILURE;
		}
		if(atexit(tidyup)) {
			emsg("could not assign exit function\n");
			freeargs(OPTNUM, args);
			return EXIT_FAILURE;
		}
		srand((unsigned)time(0));
	}
	if(n == 0) {			/*Start interactive mode*/
		freeargs(OPTNUM, args);
#ifndef NOTUI
		err = tui(file);
		if(err) {
			return EXIT_FAILURE;
		}
		return EXIT_SUCCESS;
#else
		emsg("no options given, exiting...\n");
		return EXIT_FAILURE;
#endif
	} else if((options[11] || options[12] || options[14]) && n > 1) {
		/*Some options can only be used alone*/
		emsg("too many options\n");
		freeargs(OPTNUM, args);
		return EXIT_FAILURE;
	}
	if(options[10]) {		/*Play given video*/
		err = playvideo(args[10], 0);
		freeargs(OPTNUM, args);
		if(err) {return EXIT_FAILURE;}
		return EXIT_SUCCESS;
	}
	if(options[11]) {		/*Import database*/
		err = importdb(args[11]);
		freeargs(OPTNUM, args);
		if(err) {return EXIT_FAILURE;}
		return EXIT_SUCCESS;
	}
	if(options[12]) {		/*Generate config*/
		err = writeconfig(file);
		freeargs(OPTNUM, args);
		if(err) {return EXIT_FAILURE;}
		return EXIT_SUCCESS;
	}
	if(options[9]) {free(file);}
	if(options[13]) {		/*Help text*/
		freeargs(OPTNUM, args);
		if(n <= 2) {
			showhelp(mode);
			return EXIT_SUCCESS;
		} else {
			emsg("too many options\n");
			return EXIT_FAILURE;
		}
	}
	if(options[14]) {		/*Display version*/
		showversion();
		freeargs(OPTNUM, args);
		return EXIT_SUCCESS;
	}
	if(mode >= 0 && options[0]) { /*Sync mode*/
		if(n == 1) { /*Add subscription*/
			if(!strcmp(args[0], "h") || \
			   !strcmp(args[0], "-h") || \
			   !strcmp(args[0], "--help")) {
				showhelp(0);
			} else if(args[0]) {
				err = addchannel(geturlid(args[0]), NULL);
			} else {
				emsg("missing argument\n");
				err = EXIT_FAILURE;
			}
		} else if(options[4]) { /*refresh*/
			err = syncvids();
		} else if(options[6]) { /*info*/
			/*Show channel info*/
		} else if(options[7]) { /*search*/
			/*Search for channel online*/
		} else {
			emsg("invalid option\n");
			err = EXIT_FAILURE;
		}
	} else if(mode >= 0 && options[1]) { /*Remove mode*/
		if(!strcmp(args[1], "h") || \
		   !strcmp(args[1], "-h") || \
		   !strcmp(args[1], "--help")) {
			showhelp(1);
		} else if(n == 1 && args[1]) { /*Delete subscription*/
			err = delchannel(args[1]);
		} else if(args[1]) {
			emsg("too many options\n");
			err = EXIT_FAILURE;
		} else {
			emsg("missing argument\n");
			err = EXIT_FAILURE;
		}
	} else if(mode >= 0 && options[2]) { /*Query mode*/
		if(n == 1) {
			emsg("not enough options\n");
			err = EXIT_FAILURE;
		} else if(options[6]) { /*info*/
			err = querychannel(args[6]);
		} else if(options[7]) { /*search*/
			err = showsearchresults(args[7]);
		} else {
			emsg("invalid option\n");
			err = EXIT_FAILURE;
		}
	} else if(mode >= 0 && options[3]) { /*File mode*/
		if(n == 1) {
			emsg("not enough options\n");
			err = EXIT_FAILURE;
		} else if(options[5]) { /*list*/
			/*list videos for a given channel*/
		} else if(options[6]) { /*info*/
			/*show video info*/
		} else if(options[7]) { /*search*/
			/*search for a video*/
		} else if(options[8]) { /*download*/
			err = downloadvideo(args[8], NULL);
		} else {
			emsg("invalid option\n");
			err = EXIT_FAILURE;
		}
	}
	freeargs(OPTNUM, args);
	if(err) {return EXIT_FAILURE;}
	return EXIT_SUCCESS;
}
